package com.melihelibol.stratosphere {
	
	/*
	Copyright (c) <2009> <copyright H. Melih Elibol>
	
	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:
	
	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.
	
	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
	*/
	
	import com.melihelibol.stratosphere.events.StratosphereEvent;
	import com.melihelibol.stratosphere.net.PeerConnection;
	import com.melihelibol.stratosphere.vo.PeerVOProxy;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.utils.UIDUtil;
	
	/**
	 * TODO:
	 * 
	 * Fix request connection. Final connection is mis-established.
	 * Accounting for all connection objects, except for newConnection, since client ID is not detected
	 * until later broadcasting. NetStream is no help here.
	 * 
	 * Once I solve this final issue, I will tag and build a new version with recieve stream pooling.
	 * 
	 */
	public class Stratosphere extends EventDispatcher {
		
		public static const SERVER_ADDRESS:String = "rtmfp://stratus.adobe.com/";
		
		public var developerKey:String;
		public var me:PeerVOProxy;
		
		private var peerConnections:Dictionary = new Dictionary();
		private var newConnection:PeerConnection;
		private var pendingConnections:Dictionary = new Dictionary();
		
		public function Stratosphere(developerKey:String) {
			this.developerKey = developerKey;
			initMe();
			initPropogationChannels();
			newConnection = createNewConnection();
		}
		
		private function initPropogationChannels():void {
			addMessageListener("broadcastPeers", handleBroadcastPeers);
			addMessageListener("requestConnection", handleRequestConnection);
		}
		
		private function initMe():void {
			me = new PeerVOProxy();
			me.clientID = UIDUtil.createUID();
		}
		
		/**
		 * Opens a new connection to connect to a peer
		 */
		private function createNewConnection():PeerConnection {
			var pc:PeerConnection = new PeerConnection(this);
			pc.addEventListener(PeerConnection.INIT_CONNECTION_COMPLETE, openNewComplete);
			pc.addEventListener(PeerConnection.INIT_RECIEVE_STREAM_COMPLETE, connectToPeerComplete);
			pc.open();
			return pc;
		}
		
		private function openNewComplete(e:Event):void {
			var pc:PeerConnection = PeerConnection(e.currentTarget);
			pc.removeEventListener(PeerConnection.INIT_CONNECTION_COMPLETE, openNewComplete);
			var se:StratosphereEvent = new StratosphereEvent(StratosphereEvent.NEW_CONNECTION_READY);
			se.localPeerID = pc.localPeerID;
			se.remotePeerID = pc.remotePeerID;
			dispatchEvent(se);
		}
		
		/**
		 * Create a connection to a peer.
		 * This depends on a new connection being ready.
		 * There is no error control for this at the moment.
		 */
		
		public function connectToPeer(remotePeerID:String):void {
			var pc:PeerConnection = newConnection;
			pc.receiveConnection(remotePeerID);
		}
		
		private function connectToPeerComplete(e:Event):void {
			var pc:PeerConnection = PeerConnection(e.currentTarget);
			pc.removeEventListener(PeerConnection.INIT_RECIEVE_STREAM_COMPLETE, connectToPeerComplete);
			peerConnections[pc.remotePeerID] = pc;
			addConnectionListeners(pc);
			
			if(pendingConnections[pc]){
				delete pendingConnections[pc];
			}
			if(pc === newConnection){
				newConnection = createNewConnection();
			}
			
			updateConnections(pc);
			broadcastPeers();
			
			var se:StratosphereEvent = new StratosphereEvent(StratosphereEvent.CONNECTION_SUCCESSFUL);
			se.localPeerID = pc.localPeerID;
			se.remotePeerID = pc.remotePeerID;
			dispatchEvent(se);
		}
		
		/**
		 * Broadcast a message to all peers
		 * 
		 * @param message
		 * @param o
		 */
		
		public function broadcastMessage(message:String, ...args):void {
			for each(var pc:PeerConnection in peerConnections){
				pc.send.apply(pc, [message].concat(args));
			}
		}
		
		public function broadcastFilteredMessage(message:String, filter:Array, ...args):void {
			var pc:PeerConnection;
			for each(pc in peerConnections){
				for each(var peer:Object in filter){
					if(pc.remoteClientID == peer.clientID){
						pc.send.apply(pc, [message].concat(args));
					}
				}
			}
		}
		
		/**
		 * Messaging
		 */
		
		private var _messageHandlers:Dictionary = new Dictionary();
		private var _listeners:Dictionary = new Dictionary();
		
		private function messageFunctor(message:String):Function {
			return function(args:Array):void {
				for(var listener:Function in _listeners){
					if(_listeners[listener] == message)
						listener.apply(null,args);
				}
			}
		}
		
		public function addMessageListener(message:String, listener:Function):void {
			_listeners[listener] = message;
			if(!_messageHandlers[message]){
				_messageHandlers[message] = messageFunctor(message);
				addMessageListenerToConnections(message);
			}
		}
		
		public function removeMessageListener(message:String, listener:Function):void {
			delete _listeners[listener];
		}
		
		private function addConnectionListeners(pc:PeerConnection):void {
			for(var message:String in _messageHandlers){
				pc.addMessageHandler(message, _messageHandlers[message]);
			}
		}
		
		private function removeConnectionListeners(pc:PeerConnection):void {
			for(var message:String in _messageHandlers){
				pc.removeMessageHandler(message);
			}
		}
		
		private function addMessageListenerToConnections(message:String):void {
			for each(var pc:PeerConnection in peerConnections){
				pc.addMessageHandler(message, _messageHandlers[message]);
			}
		}
		
		private function removeMessageListenerFromConnections(message:String):void {
			for each(var pc:PeerConnection in peerConnections){
				pc.removeMessageHandler(message);
			}
		}
		
		/**
		 * Support
		 */		
		
		private function updateConnections(pc:PeerConnection):void {
			me.connections.push({localPeerID:pc.localPeerID, remotePeerID:pc.remotePeerID});
		}
		
		/**
		 * Propogation channel
		 */
		
		public function broadcastPeers(filter:Array=null):void {
			if(filter){
				broadcastFilteredMessage("broadcastPeers", filter, me.object);
			} else {
				broadcastMessage("broadcastPeers", me.object);
			}
		}
		
		private function handleBroadcastPeers(peer:Object):void {
			var remotePeer:Object;
			var newPeers:Array = [];
			var broadcastFilter:Array = [];
			for each(remotePeer in peer.peers){
				if(remotePeer.clientID == me.clientID) continue;
				var exists:Boolean = false;
				for each(var localPeer:Object in me.peers){
					if(localPeer.clientID == remotePeer.clientID){
						if(remotePeer.clientID != me.clientID)
							copy(remotePeer, localPeer);
						exists = true;
					}
				}
				if(!exists){
					me.peers.push(remotePeer);
					if(isConnected(remotePeer)){
						getConnection(remotePeer).remoteClientID = remotePeer.clientID;
					}
					newPeers.push(remotePeer);
					if(peerNeedsUpdate(remotePeer))
						broadcastFilter.push(remotePeer);
				}
			}
			for each(remotePeer in me.peers){
				if(remotePeer.clientID != me.clientID && !isConnected(remotePeer)){
					requestConnection(remotePeer);
				}
			}
			//disconnectDuplicates();
			if(broadcastFilter.length) broadcastPeers(broadcastFilter);
			dispatchEvent(new StratosphereEvent(StratosphereEvent.UPDATE_PEERS));
		}
		
		//with PeerConnections rewrite, this should no longer be necessary
		private function disconnectDuplicates():void {
			var removedPeersByRemotePeerID:Array = [];
			for each(var pcA:PeerConnection in peerConnections){
				for each(var pcB:PeerConnection in peerConnections){
					if(pcA === pcB) continue;
					if(pcA.remoteClientID == pcB.remoteClientID){
						removedPeersByRemotePeerID.push(pcB.remotePeerID);
						pcB.close();
					}
				}
			}
			for(var remotePeerID:String in removedPeersByRemotePeerID){
				delete peerConnections[remotePeerID];
			}
		}
		
		private function peerNeedsUpdate(peer:Object):Boolean {
			var needsUpdate:Boolean = true;
			for each(var peerpeer:Object in peer.peers){
				if(peerpeer.clientID == me.clientID){
					needsUpdate = false;
					if(peerpeer.name != me.name)
						return true;
					if(peerpeer.connections.lenth != me.connections.length)
						return true;
					if(peerpeer.peers.lenth != me.peers.length)
						return true;
				}
			}
			return needsUpdate;
		}
		
		private function getConnection(peer:Object):PeerConnection {
			for each(var pc:PeerConnection in peerConnections){
				for each(var remoteConnection:Object in peer.connections){
					if(pc.remotePeerID == remoteConnection.localPeerID)
						return pc;
				}
			}
			return null;
		}
		
		private function isConnected(peer:Object):Boolean {
			return getConnection(peer) != null;
		}
		
		/**
		 * Request Connection logic
		 */
		
		private function requestConnection(remotePeer:Object):void {
			for each(var pendingRemotePeer:Object in pendingConnections){
				if(pendingRemotePeer.clientID == remotePeer.clientID) return;
			}
			for each(var checkPC:PeerConnection in peerConnections){
				if(checkPC.remoteClientID == remotePeer.clientID) return;
			}
			//how do we test newConnection?
			var pc:PeerConnection = createNewConnection();
			pendingConnections[pc] = remotePeer;
			if(pc.phase == PeerConnection.INIT_CONNECTION){
				pc.addEventListener(PeerConnection.INIT_CONNECTION_COMPLETE, continueRequestConnection);
			} else {
				broadcastMessage("requestConnection", me.object, pc.localPeerID, remotePeer.clientID);
			}
		}
		
		private function continueRequestConnection(e:Event):void {
			var pc:PeerConnection = PeerConnection(e.currentTarget);
			pc.removeEventListener(PeerConnection.INIT_CONNECTION_COMPLETE, continueRequestConnection);
			var remotePeer:Object = pendingConnections[pc];
			broadcastMessage("requestConnection", remotePeer, me.object, pc.localPeerID);
		}
		
		private function handleRequestConnection(toPeer:Object, fromPeer:Object, fromPeerID:String):void {
			if(me.clientID == toPeer.clientID){
				for each(var pendingRemotePeer:Object in pendingConnections){
					if(pendingRemotePeer.clientID == fromPeer.clientID) return;
				}
				for each(var checkPC:PeerConnection in peerConnections){
					if(checkPC.remoteClientID == fromPeer.clientID) return;
				}
				//how do we test newConnection?
				connectToPeer(fromPeerID);
			} else {
				for each(var peer:Object in me.peers){
					if(peer.clientID == toPeer.clientID){
						broadcastFilteredMessage("requestConnection", [toPeer], toPeer, fromPeer, fromPeerID);
						return;
					}
				}
				var filter:Array = removeDuplicates(me.peers, fromPeer.peers, [], "clientID");
				broadcastFilteredMessage("requestConnection", filter, toPeer, fromPeer, fromPeerID);
			}
		}
		
		/**
		 * UTILS
		 */
		
		/**
		 * 
		 * @param a
		 * @param b
		 * @param exclude
		 * @param field
		 * @return 
		 * 
		 */		
		
		private function removeDuplicates(a:Array, b:Array, exclude:Array, field:String):Array {
			var r:Array = [];
			for(var i:int = -1;++i<a.length;){
				var add:Boolean = true;
				for(var j:int = -1;++j<b.length;){
					if(a[i][field] == b[j][field]){
						add = false;
						break;
					}
				}
				if(add){
					r.push(a[i]);
				}
			}
			if(exclude.length){
				for(var q:int = r.length;--q>-1;){
					for(var k:int = -1;++k<exclude.length;){
						if(r[q][field] == exclude[k][field]){
							r.splice(q, 1);
						}
					}
				}
			}
			return r;
		}
		
		/**
		 * 
		 * @param fromA
		 * @param toB
		 * 
		 */		
		
		private function copy(fromA:Object, toB:Object):void {
			for(var i:String in toB){
				toB[i] = fromA[i];
			}
		}
		
	}
	
}